/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;



/******************************************************************************/
/**
   An output stream.
   <p>

   IMPORTANT: Many of the methods here have to be public so that
   {@link OutputStreamManager} can get at them.  In general, however,
   you should call them only from that class, not directly.
   <p>

   This is intended as a base class, with specific types inheriting from it,
   of which the most likely type is a file output class.
   <p>

   The class supports the possibility that output to this particular stream
   may be suspended temporarily, which you achieve using {@link setIgnoreData}.
   <p>

   It also provides facilities to enable you to buffer data internally.  This
   may be useful if, for example, you are sending output to a file, but need
   to retain a particular portion of the output for a while, in order to
   output stuff in a slightly different order.
   <p>

   To this end, there are a collection of methods which let you start and
   stop buffering, flush the buffer to the actual output stream, get and
   set the contents of the buffer, etc.  Note that newlines sent to the
   buffer are not actually stored there as such -- instead I use a special
   marker (the reasons for which I have now forgotten, but which I think
   makes pattern-matching easier).  If you wish to search for newlines,
   therefore, you actually need to look for {@link C_PseudoLineSeparatorAsPat}.
   <p>

   Inheriting classes will probably need to override all of the protected
   methods below.
   <p>

   The <i>doOpen</i> and <i>doClose</i> methods are invoked only if there is any
   data actually to be written out.
   <p>

   <i>writeHeader</i> and <i>writeTrailer</i> are invoked immediately after
   the call to <i>doOpen</i> and immediately before the call to <i>doClose</i>.
   If they write anything out, they should probably call <i>write</> rather
   than <i>doWrite</i>.
   <p>
 
   Internal buffering is turned off at the point you open a stream, and also
   just before closing it (so that any trailer will actually be written to the
   stream, and not buffered).  Note that buffered data is <i>not</i>
   automatically flushed to the output stream at the time the stream is closed.
**/
   

public class MyOutputStream
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Public                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /** In the <i>buffered</i> output (only) newline characters are represented
      by a special character (for reasons which I can no longer recall, but
      which I think are to do with simplifying the task of pattern matching).
      If you need to do searches for this character, you need to look for
      C_PseudoLineSeparatorAsPat.  */

  public static final String C_PseudoLineSeparatorAsPat = "\\u0008";
  public static final String C_LineSeparator = System.getProperty("line.separator");
  public static final String C_PseudoLineSeparatorAsString =  "\u0008";
  

  /****************************************************************************/
  /**
   * Constructor.
   */
  
  public MyOutputStream () throws IOException
  {
    m_MemoryBuffer = new BufferedWriterWithUndo();
  }
  
  
  /****************************************************************************/
  /**
     Clears any buffered data.
  */

  public void clearMemoryBufferContents ()
  {
    setMemoryBufferContents("");
  }


  /****************************************************************************/
  /**
     Closes the stream.

     @throws IOException
  */

  public void close () throws IOException
  {
    setMemoryBuffering(false);
    if (m_HeaderWritten) { writeTrailer(); }
    doClose();
    m_HeaderWritten = false;
    clearMemoryBufferContents();
  }


  /****************************************************************************/
  /**
     Extracts from the buffer the zero'th matching group based on a given
     pattern.  The extracted string is returned, and is also removed from
     the buffer.  This <i>probably</i> makes sense only when buffering
     to memory, which is why I've restricted it to that use.
     
     @param pattern Pattern to match.  The zero'th matching group is returned,
                    and is also extracted from the buffer.

     @return Buffer contents.
   
     @throws IOException
  */

  public String extractMemoryBufferContents (String pattern) throws IOException
  {
    String s = m_MemoryBuffer.getBufferContents();
    Pattern pat = Pattern.compile(pattern);
    Matcher matcher = pat.matcher(s);
    if (!matcher.find()) return "";
    String res = matcher.group(0);
    m_MemoryBuffer.setBufferContents(matcher.replaceFirst(""));
    return res;
  }


  /****************************************************************************/
  /**
   * Flushes the memory buffer contents to the output stream.
   */
  
  public void flushMemoryBufferToOutputStream () throws IOException
  {
    m_OutputStream.write(getMemoryBufferContents());
    clearMemoryBufferContents();
  }
  
  
  /****************************************************************************/
  /**
     Obtain the contents of the memory buffer.

     @return Buffer contents.
   
     @throws IOException
  */

  public String getMemoryBufferContents () throws IOException
  {
    return m_MemoryBuffer.getBufferContents(); 
  }


  /****************************************************************************/
  /**
     Returns an indication of whether output is going to the in-memory buffer.
   
     @return True if output is going to the in-memory buffer.
   */
  
  public boolean getMemoryBuffering ()
  {
    return m_CurrentOutput == m_MemoryBuffer;
  }
  
  
  /****************************************************************************/
  /**
     Gets the encoding (eg "UTF-8") associated with the stream.

     @return Encoding.
  */
  
  public String getEncoding ()
  {
    return m_Encoding;
  }


  /****************************************************************************/
  /**
     Gets the name associated with the stream.  For a file stream (which is the
     main thing I currently anticipate using all of this for), it will be the
     full path for the file.

     @return Name.
  */
  
  public String getName ()
  {
    return m_Name;
  }


  /****************************************************************************/
  /**
     Returns an indication of whether the stream is open.

     @return True if open.
  */

  public boolean isOpen ()
  {
    return m_HeaderWritten;
  }

  
  /****************************************************************************/
  /**
     Outputs a newline.
   
     @throws IOException
  */

  public void newLine () throws IOException
  {
    write(m_CurrentOutput == m_OutputStream ? C_LineSeparator : C_PseudoLineSeparatorAsString);
  }


  /****************************************************************************/
  /**
     Returns characters from the end of the active output buffer.

     @param nChars Number of characters (or is that code points?).

     @return String containing peeked characters.
  */

  public String peekEndOfData (int nChars)
  {
    return m_CurrentOutput.getEndOfBufferContents(nChars);
  }

  
  /****************************************************************************/
  /**
     Removes a single instance of the specified string from the buffer.
    
     @param s String to trim off.
    
     @return True if a trailing instance was found.
   */

  public boolean removeTrailingStringFromBufferOnce (String s)
  {
    return m_CurrentOutput.removeTrailing(s);
  }


  /****************************************************************************/
  /**
     Removes all trailing instances of a given string from the buffer.
    
     @param s String to remove.
    
     @return True if anything has been done.
   */

  public boolean removeTrailingStringFromBufferRepeated (String s)
  {
    return m_CurrentOutput.removeTrailingAll(s);
  }


  /****************************************************************************/
  /**
   * Replace all occurrences of a given pattern with another string in whatever
   * buffer we are currently writing to (current destination).
   * 
   * @param pat Pattern.
   * @param replaceWith
   */
  
  public void replaceAll (String pat, String replaceWith)
  {
    m_CurrentOutput.replaceAll(pat, replaceWith);
  }
  
  
  /****************************************************************************/
  /**
     Sets the encoding.

     @param encoding eg UTF-8
   */

  public void setEncoding (String encoding)
  {
    m_Encoding = encoding;
  }
  
  
  /****************************************************************************/
  /**
     Controls whether data is written out or simply thrown away.

     @param ignoreData
  */

  public void setHeaderAndTrailerWriter (OutputStreamHeaderAndTrailerWriter w)
  {
    m_HeaderAndTrailerWriter = w;
  }


  /****************************************************************************/
  /**
     Controls whether data is written out or simply thrown away.

     @param ignoreData
  */

  public void setIgnoreData (boolean ignoreData)
  {
    if (ignoreData)
      ++m_IgnoreData;
    else
      --m_IgnoreData;
  }


  /****************************************************************************/
  /**
     Sets the buffer contents.
    
     @param s 
  */

  public void setMemoryBufferContents (String s)
  {
    m_MemoryBuffer.setBufferContents(s);
  }


  /****************************************************************************/
  /**
   * Determines whether output should go to the genuine output stream or to
   * the internal buffer.
   * 
   * @param doMemoryBuffering True if data is to be buffered.
   */
  
  public void setMemoryBuffering (boolean doMemoryBuffering)
  {
    m_CurrentOutput = doMemoryBuffering ? m_MemoryBuffer : m_OutputStream;
  }
  
  
  /****************************************************************************/
  /**
     Writes a string to the stream.

     @param s

     throws IOException
  */

  public void write (String s) throws IOException
  {
    if (m_IgnoreData > 0) return;
    
    if (!m_HeaderWritten)
    {
      doOpen();
      boolean wasBuffering = getMemoryBuffering();
      setMemoryBuffering(false);
      m_HeaderWritten = true;
      writeHeader();
      if (wasBuffering) setMemoryBuffering(true);
    }
    
    doWrite(s);
  }




  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Protected                                **/
  /**                       Probably needs overriding                        **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Opens the stream. */
  
  protected void doOpen () throws IOException
  {
  }


  /****************************************************************************/
  protected String m_Encoding = "UTF-8";
  protected OutputStreamHeaderAndTrailerWriter m_HeaderAndTrailerWriter = null;
  protected String m_Name = "";
  protected BufferedWriterWithUndo m_OutputStream = null;


 
  

  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Closes the stream. */
  
  private void doClose () throws IOException
  {
    m_OutputStream.close();
  }

  
  /****************************************************************************/
  /* Writes out a given string. */
  
  private void doWrite (String s) throws IOException
  {
    m_CurrentOutput.write(s);
  }

   /****************************************************************************/
  /* Writes out any required header. */
  
  private void writeHeader () throws IOException
  {
    if (null != m_HeaderAndTrailerWriter) m_HeaderAndTrailerWriter.writeHeader(this);
  }


  /****************************************************************************/
  /* Writes out any required trailer. */
  
  private void writeTrailer () throws IOException
  {
    if (null != m_HeaderAndTrailerWriter && !m_TrailerWritten)
    {
      m_HeaderAndTrailerWriter.writeTrailer(this);
      m_TrailerWritten = true;
    }
  }

  
  
  

  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  protected BufferedWriterWithUndo m_CurrentOutput = null;
  private boolean m_HeaderWritten = false;
  private BufferedWriterWithUndo m_MemoryBuffer = null;
  private boolean m_TrailerWritten = false;
  private int m_IgnoreData = 0;
}
